unit Shopping;

interface

uses
  Classes, Items, Decryptor, GlobalRecordsAndEnums;

type
  TShoppingItem = class
    private
      _decryptor: TDecryptor; // if kind = SK_Invention, there may be a decryptor. Otherwise, it should be nil
      _newitem: TNewItem;

      _amount: Integer;
      _kind: EShoppingKind;
      _me: Integer;
      _extrawastemodifier: Double;

      _decryptorID: Integer; // used when loading from file: At this point you can't get the decryptor, so let TInventionCalc assign you one based on this ID
      _newitemID: Integer; // used when loading from file: Same as the decryptor
    public
      constructor Create(item: TNewItem; amount: Integer; kind: EShoppingKind; me: Integer = 0; extrawastemodifier: Double = 1; decryptor: TDecryptor = nil); overload;
      constructor Create(sshopitem: SShoppingitem); overload;

      function Save: SShoppingItem;

      property Item: TNewItem read _newitem write _newitem; // Should only be written when loading from file
      property Decryptor: TDecryptor read _decryptor write _decryptor; // used when Kind = SK_Invention, otherwise nil. Should only be written when loading from file
      property Amount: Integer read _amount write _amount;
      property Kind: EShoppingKind read _kind;
      property ME: Integer read _me; // used when Kind = SK_Manufacture, otherwise 0
      property ExtraWasteModifier: Double read _extrawastemodifier; // used when Kind = SK_Manufacture, otherwise 1

      property DecryptorID: Integer read _decryptorID;
      property ItemID: Integer read _newitemID;
  end;

  TShoppingList = class
    private
      // the three lists all hold TShoppingItem
      _items: TList; // fills lvItems
      _buy: TList; // fills lv Buy
      _manufacture: TList; // fills lvManufacture

      function GetCount: Integer;
      function GetBuyCount: Integer;
      function GetManufactureCount: Integer;

      function LoadShoppingList_v041(stream: TMemoryStream): Boolean;
      procedure AddSubitems(shopitem: TShoppingItem);
      procedure CompressLists;
      procedure CompressList(list: TList);

      procedure RemoveSubitems(shopitem: TShoppingItem);
      function RemoveSimilarItem(list: TList; shopitem: TShoppingItem): Boolean;
    public
      constructor Create;
      destructor Destroy; reintroduce;

      function GetItemByIndex(index: Integer): TShoppingItem;
      function GetBuyItemByIndex(index: Integer): TShoppingItem;
      function GetManufactureItemByIndex(index: Integer): TShoppingItem;
      procedure AddItem(shopitem: TShoppingItem);

      procedure RemoveItem(shopitem: TShoppingItem);
      procedure Clear;

      property Count: Integer read GetCount;
      property BuyCount: Integer read GetBuyCount;
      property ManufactureCount: Integer read GetManufactureCount;
  end;

implementation

uses
  Math, SysUtils, Dialogs, Forms;

//===========================
//====== TShoppingItem ======
//===========================

constructor TShoppingItem.Create(item: TNewItem; amount: Integer; kind: EShoppingKind; me: Integer = 0; extrawastemodifier: Double = 1; decryptor: TDecryptor = nil);
begin
  inherited Create;
  _decryptor := decryptor;
  _newitem := item;
  _amount := amount;
  _kind := kind;
  _me := me;
  _extrawastemodifier := extrawastemodifier;
end;

constructor TShoppingItem.Create(sshopitem: SShoppingItem);
begin
  inherited Create;
  _decryptor := nil;
  _decryptorID := sshopitem.DecryptorID;
  _newitem := nil;
  _newitemID := sshopitem.NewItemID;
  _amount := sshopitem.Amount;
  _kind := sshopitem.Kind;
  _me := sshopitem.ME;
  _extrawastemodifier := sshopitem.Extrawastemodifier;
end;

function TShoppingItem.Save: SShoppingItem;
begin
  if (_decryptor <> nil) then result.DecryptorID := _decryptor.ID
  else result.DecryptorID := -1;
  if (_newitem <> nil) then result.NewItemID := _newitem.ID
  else result.NewItemID := -1;
  result.Amount := _amount;
  result.Kind := _kind;
  result.ME := _me;
  result.Extrawastemodifier := _extrawastemodifier;
end;

//===========================
//====== TShoppingList ======
//===========================

function TShoppingList.GetCount: Integer;
begin
  result := _items.Count;
end;

function TShoppingList.GetBuyCount: Integer;
begin
  result := _buy.Count;
end;

function TShoppingList.GetManufactureCount: Integer;
begin
  result := _manufacture.Count;
end;

constructor TShoppingList.Create;
var
  stream: TMemoryStream;
  version: Double;
begin
  inherited Create;
  _items := TList.Create;
  _buy := TList.Create;
  _manufacture := TList.Create;

  // load from file
  if (not FileExists('user\Shoppinglist.dat')) then begin
    MessageDlg('Unable to locate the file ' + ExtractFileDir(Application.ExeName) + '\user\Shoppinglist.dat.', mtError, [mbOk], 0);
  end
  else begin
    stream := TMemoryStream.Create;
    stream.Position := 0;
    stream.LoadFromFile('user\Shoppinglist.dat');
    try
      stream.Read(version, sizeof(Double));
    except
      MessageDlg('Error reading Shoppinglist.dat: invalid version information.', mtError, [mbOk], 0);
      stream.Free;
      exit;
    end;
    if (Round(version * 100) = 41) then // just 0.41 seems to result in a precision error
      self.LoadShoppingList_v041(stream)
    else begin
      MessageDlg('Error reading Shoppinglist.dat: no suitable load function found.', mtError, [mbOk], 0);
    end;
    stream.Free;
  end;
end;

destructor TShoppingList.Destroy;
var
  i: Integer;
  stream: TMemoryStream;
  sshopitem: SShoppingItem;
  version: Double;
begin
  version := 0.41;
  stream := TMemoryStream.Create;
  stream.Write(version, SizeOf(Double));
  for i := 0 to _items.Count -1 do begin
    sshopitem := TShoppingItem(_items[i]).Save;
    sshopitem.ShopType := ST_Item;
    stream.Write(sshopitem, SizeOf(SShoppingItem));
  end;
  for i := 0 to _buy.Count -1 do begin
    sshopitem := TShoppingItem(_buy[i]).Save;
    sshopitem.ShopType := ST_Buy;
    stream.Write(sshopitem, SizeOf(SShoppingItem));
  end;
  for i := 0 to _manufacture.Count -1 do begin
    sshopitem := TShoppingItem(_manufacture[i]).Save;
    sshopitem.ShopType := ST_Manufacture;
    stream.Write(sshopitem, SizeOf(SShoppingItem));
  end;
  try
    stream.SaveToFile('user\Shoppinglist.dat');
  except
    on e: Exception do MessageDlg(e.Message, mtError, [mbOk], 0);
  end;
  stream.Free;


  for i := 0 to _items.Count -1 do begin
    TShoppingItem(_items[i]).Destroy;
  end;
  _items.Destroy;
  for i := 0 to _buy.Count -1 do begin
    TShoppingItem(_buy[i]).Destroy;
  end;
  _buy.Destroy;
  for i := 0 to _manufacture.Count -1 do begin
    TShoppingItem(_manufacture[i]).Destroy;
  end;
  _manufacture.Destroy;

  inherited Destroy;
end;

function TShoppingList.LoadShoppingList_v041(stream: TMemoryStream): Boolean;
var
  sshopitem: SShoppingItem;
  shopitem: TShoppingItem;
begin
  result := true;
  while stream.Position < stream.Size do begin
    try
      stream.Read(sshopitem, sizeof(SShoppingItem));
      shopitem := TShoppingItem.Create(sshopitem);
      // shopitem.parent := iets
      case sshopitem.ShopType of
        ST_Item: _items.Add(shopitem);
        ST_Buy: _buy.Add(shopitem);
        ST_Manufacture: _manufacture.Add(shopitem);
      end;
    except
      MessageDlg('Error reading Shoppinglist.dat: invalid item', mtError, [mbOk], 0);
      result := false;
      exit;
    end;
  end;
end;

function TShoppingList.GetItemByIndex(index: Integer): TShoppingItem;
begin
  if (index < _items.Count) then result := TShoppingItem(_items[index])
  else result := nil;
end;

function TShoppingList.GetBuyItemByIndex(index: Integer): TShoppingItem;
begin
  if (index < _buy.Count) then result := TShoppingItem(_buy[index])
  else result := nil;
end;

function TShoppingList.GetManufactureItemByIndex(index: Integer): TShoppingItem;
begin
  if (index < _manufacture.Count) then result := TShoppingItem(_manufacture[index])
  else result := nil;
end;

procedure TShoppingList.AddItem(shopitem: TShoppingItem);
// Adds an item to the shopping list (obviously). Also adds its subitems to the respective lists
begin
  // add item to lvItems
  _items.Add(shopitem);

  // add subitems that should be purchased or manufactured. If it is an invention, the GUI will handle it
  if (shopitem.Kind = SK_Manufacture) then begin
     AddSubitems(shopitem);
  end;
  CompressLists;
end;

procedure TShoppingList.AddSubitems(shopitem: TShoppingItem);
// adds the subitems of the given item to be manufactured (or purchased if there are nu subitems)
var
  i: Integer;
  subitem: TShoppingItem;
  requirementslist: SRequirementsList;
begin
  requirementsList := shopitem.Item.GetRequirementsList(shopitem.Amount, shopitem.ME, ACT_Manufacture, shopitem.ExtraWasteModifier);
  for i := 0 to Length(requirementslist) -1 do begin
    subitem := TShoppingItem.Create(requirementslist[i].Item, requirementslist[i].Amount, SK_Manufacture, requirementslist[i].Item.BPOME, shopitem.ExtraWasteModifier);
    if ((requirementslist[i].Item.Manufacture) and (requirementslist[i].Item.CanBeManufactured)) then begin // check if the user specified if the item should be manufactured, as well as whether it is actually possible to manufacture it
      AddSubitems(subitem);
      _manufacture.Add(subitem);
    end
    else begin
      _buy.Add(subitem);
    end;
  end;
end;

procedure TShoppingList.CompressLists;
begin
  CompressList(_items);
  CompressList(_buy);
  CompressList(_manufacture);
end;

procedure TShoppingList.CompressList(list: TList);
// compresses the given TList item, so that there are no duplicates in them.
var
  i, j: Integer;
  item1, item2, newitem: TShoppingItem;
begin
  i := 0;
  while i <= list.Count -1 do begin
    item1 := TShoppingItem(list[i]);
    for j := i + 1 to list.Count -1 do begin
      item2 := TShoppingItem(list[j]);
      if ((item1.Item = item2.Item) and (item1.Decryptor = item2.Decryptor)) then begin // decryptor check is needed for invention shopping items. Items with different decryptors obviously can't be merged
        newitem := TShoppingItem.Create(item1.Item, item1.Amount + item2.Amount, item1.Kind, Max(item1.ME, item2.ME), Max(item1.ExtraWasteModifier, item2.ExtraWasteModifier));
        list.Remove(item1);
        list.Remove(item2);
        list.Add(newitem);
        i := i - 1; // you just deleted an item, so decrease i by 1 in order not to skip the next item (because at the end of the while-loop, it'll be increased again)
        break;
      end;
    end;
    i := i + 1;
  end;
end;

procedure TShoppingList.RemoveItem(shopitem: TShoppingItem);
var
  i: Integer;
begin
  for i := 0 to _items.Count -1 do begin
    if (_items[i] = shopitem) then begin
      _items.Remove(shopitem);
      if (shopitem.Kind = SK_Manufacture) then begin
        RemoveSubitems(shopitem);
      end;
      break;
    end;
  end;
  shopitem.Destroy;
end;

procedure TShoppingList.RemoveSubitems(shopitem: TShoppingItem);
var
  i: Integer;
  subitem: TShoppingItem;
  requirementslist: SRequirementsList;
begin
  requirementsList := shopitem.Item.GetRequirementsList(shopitem.Amount, shopitem.ME, ACT_Manufacture, shopitem.ExtraWasteModifier);
  for i := 0 to Length(requirementslist) -1 do begin
    subitem := TShoppingItem.Create(requirementslist[i].Item, requirementslist[i].Amount, SK_Manufacture, requirementslist[i].Item.BPOME, shopitem.ExtraWasteModifier);
    if ((requirementslist[i].Item.Manufacture) and (requirementslist[i].Item.CanBeManufactured)) then begin
      RemoveSubitems(subitem);
      if (not RemoveSimilarItem(_manufacture, subitem)) then begin
        // if the item could not be removed, it might be because the user changed the "manufacture" setting for this specific item
        RemoveSimilarItem(_buy, subitem);
      end;
    end
    else begin
      if (not RemoveSimilarItem(_buy, subitem)) then begin
        // if the item could not be removed, it might be because the user changed the "manufacture" setting for this specific item
        RemoveSubitems(subitem);
        RemoveSimilarItem(_manufacture, subitem);
      end;
    end;
  end;
end;

function TShoppingList.RemoveSimilarItem(list: TList; shopitem: TShoppingItem): Boolean;
// "removes" a given shopitem from the given list. It actually only substracts amounts, and removes the item if the amount has become 0 (because of list compression)
// returns whether or not it actually "removed" the item
var
  i: Integer;
  item: TShoppingItem;
begin
  result := false;
  for i := 0 to list.Count -1 do begin
    item := TShoppingItem(list[i]);
    if ((item.Item = shopitem.Item) and (item.Decryptor = shopitem.Decryptor)) then begin
      item.Amount := item.Amount - shopitem.Amount;
      if (item.Amount <= 0) then begin
        list.Remove(item);
        item.Destroy;
      end;
      result := true;
      break;
    end;
  end;
end;

procedure TShoppingList.Clear;
var
  i: Integer;
begin
  for i := 0 to _items.Count -1 do begin
    TShoppingItem(_items[i]).Destroy;
  end;
  for i := 0 to _buy.Count -1 do begin
    TShoppingItem(_buy[i]).Destroy;
  end;
  for i := 0 to _manufacture.Count -1 do begin
    TShoppingItem(_manufacture[i]).Destroy;
  end;
  _items.Clear;
  _buy.Clear;
  _manufacture.Clear;
end;

end.
